/*
 * Copyright 2007-2009 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package net.paoding.rose.web.impl.thread;

import static org.springframework.validation.BindingResult.MODEL_KEY_PREFIX;

import java.io.UnsupportedEncodingException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import net.paoding.rose.web.annotation.HttpFeatures;
import net.paoding.rose.web.annotation.Intercepted;
import net.paoding.rose.web.annotation.ParamResolver;
import net.paoding.rose.web.annotation.Return;
import net.paoding.rose.web.impl.module.Module;
import net.paoding.rose.web.impl.module.NamedControllerInterceptor;
import net.paoding.rose.web.impl.validation.ParameterBindingResult;
import net.paoding.rose.web.instruction.Text;
import net.paoding.rose.web.paramresolver.MethodParameterResolver;
import net.paoding.rose.web.paramresolver.ParameterNameDiscovererImpl;
import net.paoding.rose.web.paramresolver.Resolver;
import net.paoding.rose.web.paramresolver.ResolverFactoryImpl;
import net.paoding.rose.web.paramresolver.ResolverInstances;

import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.ParameterNameDiscoverer;

/**
 * @author zhiliang.wang [qieqie.wang@paoding.net]
 */
public final class ActionEngine implements Engine {

    private static Log logger = LogFactory.getLog(ActionEngine.class);

    private ControllerEngine controllerEngine;

    private Method method;

    private NamedControllerInterceptor[] registeredInterceptors;

    private MethodParameterResolver methodParameterResolver;

    private transient String toStringCache;

    public ActionEngine(ControllerEngine controllerEngine, Method method) {
        this.controllerEngine = controllerEngine;
        this.method = method;
        init();
    }

    private String methodToString() {
        return method.toString();
    }

    public ControllerEngine getControllerEngine() {
        return controllerEngine;
    }

    public NamedControllerInterceptor[] getRegisteredInterceptors() {
        return registeredInterceptors;
    }

    public Method getMethod() {
        return method;
    }

    public String[] getParameterNames() {
        return methodParameterResolver.getParameterNames();
    }

    private void init() {
        Module module = controllerEngine.getModule();
        NamedControllerInterceptor[] interceptors = module.getInterceptors();
        ArrayList<NamedControllerInterceptor> registeredInterceptors = new ArrayList<NamedControllerInterceptor>(
                interceptors.length);
        for (int i = 0; i < interceptors.length; i++) {
            if (interceptors[i].getController() != null) {
                // 1、controller自己声明的拦截器，只作用自己
                if (controllerEngine.getController() != interceptors[i].getController()) {
                    continue;
                }
            }

            // 2、如果拦截器定义必须标注某些annoation才要拦截的...
            // 那么只有标注了这些annotation的才拦截，不标注的不拦截
            String name = interceptors[i].getName();
            String nameForUser = name;
            if (nameForUser.indexOf('.') != -1) {
                nameForUser = nameForUser.replace(controllerEngine.getControllerPath() + ".",
                        "this.");
            }
            Class<? extends Annotation>[] interceptorAnnotationRequired = interceptors[i]
                    .getAnnotationClasses();
            if (interceptorAnnotationRequired != null && interceptorAnnotationRequired.length > 0) {
                for (Class<? extends Annotation> annotationClazz : interceptorAnnotationRequired) {
                    if (annotationClazz == null) {
                        continue;
                    }
                    Annotation interceptorRequiredAnnotation = method
                            .getAnnotation(annotationClazz);
                    if (interceptorRequiredAnnotation == null) {
                        // 对于标注@Inherited的annotation，getAnnotation可以保证：如果本类没有，自动会从父类判断是否具有
                        interceptorRequiredAnnotation = controllerEngine.getControllerClass()
                                .getAnnotation(annotationClazz);
                    }
                    if (interceptorRequiredAnnotation == null) {
                        // don't intercept it
                    } else {
                        registeredInterceptors.add(interceptors[i]);
                        if (logger.isDebugEnabled()) {
                            logger.debug("register interceptor" + " (by intercetor annotation) '"
                                    + name + "' for " + methodToString());
                        }
                        break;
                    }
                }
                // continue
                continue;
            }
            // 3、拦截器没有要求特有的annotation的，这种情况通过@Intercepted来标注
            Intercepted intercepted = method.getAnnotation(Intercepted.class);
            if (intercepted == null) {
                Class<?> clazz = controllerEngine.getControllerClass();
                // 对于标注@Inherited的annotation，class.getAnnotation可以保证：如果本类没有，自动会从父类判断是否具有
                intercepted = clazz.getAnnotation(Intercepted.class);
            }
            // 没有标注@Intercepted注解的，所有拦截器都能拦截
            if (intercepted == null) {
                if (interceptors[i].isApplicationContextInterceptor()) {
                    registeredInterceptors.add(interceptors[i]);
                    if (logger.isDebugEnabled()) {
                        logger.debug("register global interceptor '" + name + "' for "
                                + methodToString());
                    }
                } else {
                    registeredInterceptors.add(interceptors[i]);
                    if (logger.isDebugEnabled()) {
                        logger.debug("register interceptor '" + name + "' for " + methodToString());
                    }
                }
            }
            // 标注@Intercepted来标注注解的
            else {
                // 3.1先排除deny禁止的
                if (ArrayUtils.contains(intercepted.deny(), "*")
                        || ArrayUtils.contains(intercepted.deny(), nameForUser)) {
                    if (logger.isDebugEnabled()) {
                        logger.debug("(1)igonre interceptor '" + nameForUser + "' for "
                                + methodToString());
                    }
                    continue;
                }
                // 3.2确认最大的allow允许
                if (intercepted.allow().length > 0) {
                    if (ArrayUtils.contains(intercepted.allow(), "*")
                            || ArrayUtils.contains(intercepted.allow(), nameForUser)) {
                        registeredInterceptors.add(interceptors[i]);
                        if (logger.isDebugEnabled()) {
                            logger.debug("register interceptor '" + name + "' for " + "."
                                    + methodToString());
                        }
                    } else {
                        if (logger.isDebugEnabled()) {
                            logger.debug("(2)igonre interceptor '" + name + "' for "
                                    + methodToString());
                        }
                    }
                }
            }
        }
        //
        this.registeredInterceptors = registeredInterceptors
                .toArray(new NamedControllerInterceptor[registeredInterceptors.size()]);
        //
        ParameterNameDiscoverer parameterNameDiscoverer = new ParameterNameDiscovererImpl();
        ResolverFactoryImpl resolverFactory = new ResolverFactoryImpl();
        for (Resolver resolver : this.getCustomerResolvers()) {
            resolverFactory.addCustomerResolver(resolver);
        }
        for (Resolver resolver : controllerEngine.getCustomerResolvers()) {
            resolverFactory.addCustomerResolver(resolver);
        }
        for (Resolver resolver : module.getCustomerResolvers()) {
            resolverFactory.addCustomerResolver(resolver);
        }
        methodParameterResolver = new MethodParameterResolver(method, parameterNameDiscoverer,
                resolverFactory);
    }

    private List<Resolver> getCustomerResolvers() {
        ArrayList<Resolver> resolvers = new ArrayList<Resolver>(2);
        ParamResolver paramResolver = method.getAnnotation(ParamResolver.class);
        if (paramResolver != null) {
            for (Class<? extends Resolver> clazz : paramResolver.value()) {
                resolvers.add(ResolverInstances.getInstance(clazz));
            }
        }
        Class<?>[] parameterTypes = this.method.getParameterTypes();
        for (Class<?> parameterType : parameterTypes) {
            // only decleared annotations
            for (Annotation annotation : parameterType.getDeclaredAnnotations()) {
                if (annotation instanceof ParamResolver) {
                    for (Class<? extends Resolver> clazz : ((ParamResolver) annotation).value()) {
                        resolvers.add(ResolverInstances.getInstance(clazz));
                    }
                }
            }

        }
        return Collections.unmodifiableList(resolvers);
    }

    public boolean match(final InvocationBean invocation) {
        return true;
    }

    public Object invoke(final InvocationBean invocation) throws Throwable {

        for (String matchResultParam : invocation.getActionMatchResult().getParameterNames()) {
            invocation.getModel().add(matchResultParam,
                    invocation.getActionMatchResult().getParameter(matchResultParam));
        }

        // creates parameter binding result (not bean, just simple type, like int, Integer, int[] ...
        ParameterBindingResult paramBindingResult = new ParameterBindingResult(invocation);
        String paramBindingResultName = MODEL_KEY_PREFIX + paramBindingResult.getObjectName();
        invocation.getModel().add(paramBindingResultName, paramBindingResult);

        // resolves method parameters, adds the method parameters to model
        Object[] parameters = methodParameterResolver.resolve(invocation, paramBindingResult);
        invocation.setMethodParameters(parameters);
        String[] parameterNames = methodParameterResolver.getParameterNames();
        for (int i = 0; i < parameterNames.length; i++) {
            if (parameterNames[i] != null && parameters[i] != null
                    && invocation.getModel().get(parameterNames[i]) != parameters[i]) {
                invocation.getModel().add(parameterNames[i], parameters[i]);
            }
        }

        // applies http features before the invocation
        applyHttpFeatures(invocation);

        // returned by interceptors or action
        Object instruction = null;
        int intercetporIndex = 0;

        // invokes before-interceptors
        while (intercetporIndex < registeredInterceptors.length) {

            NamedControllerInterceptor interceptor = registeredInterceptors[intercetporIndex++];

            // returned by before method
            instruction = interceptor.before(invocation);

            if (logger.isDebugEnabled()) {
                logger.debug("intercepted and return '" + instruction + "': "
                        + interceptor.getName() + "=" //
                        + interceptor.getInner().getClass().getName());
            }

            if (instruction != null && !Boolean.TRUE.equals(instruction)) {
                // the invocation is broken

                // if false, don't render anything
                if (Boolean.FALSE.equals(instruction)) {
                    instruction = Text.plain("");
                }
                intercetporIndex = -intercetporIndex;
                break; // just break, don't return
            }
        }

        if (intercetporIndex < 0) {
            intercetporIndex = -intercetporIndex;
            invocation.setExecutedInterceptorIndex(intercetporIndex);
        } else {
            invocation.setExecutedInterceptorIndex(intercetporIndex); // just copy the setXxx Code
            // invoke the action
            final Object controller = controllerEngine.getController();
            if (controllerEngine.isProxiedController()) {
                instruction = Proxy.getInvocationHandler(controller).invoke(controller, method,
                        invocation.unsafedGetMethodParameters());
            } else {
                instruction = method.invoke(controller, invocation.unsafedGetMethodParameters());
            }
            if (instruction == null && method.getReturnType() == void.class) {
                Return returnAnnotation = method.getAnnotation(Return.class);
                if (returnAnnotation != null) {
                    instruction = returnAnnotation.value();
                }
            }
        }

        Object orginInstruction = instruction;

        // after the invocation
        for (int i = intercetporIndex - 1; i >= 0; i--) {
            instruction = registeredInterceptors[i].after(invocation, instruction);
            // 拦截器返回null的，要恢复为原instruction
            // 这个功能非常有用!!
            if (instruction == null) {
                instruction = orginInstruction;
            }
        }
        return instruction;
    }

    private void applyHttpFeatures(final InvocationBean invocation)
            throws UnsupportedEncodingException {
        HttpServletRequest request = invocation.getRequest();
        HttpServletResponse response = invocation.getResponse();
        HttpFeatures httpFeatures = method.getAnnotation(HttpFeatures.class);
        if (httpFeatures == null) {
            httpFeatures = controllerEngine.getControllerClass().getAnnotation(HttpFeatures.class);
        }
        if (httpFeatures != null) {
            if (StringUtils.isNotBlank(httpFeatures.contentType())) {
                response.setContentType(httpFeatures.contentType());
                if (logger.isDebugEnabled()) {
                    logger.debug("set response.contentType by metadata:"
                            + response.getContentType());
                }
            }
            if (StringUtils.isNotBlank(httpFeatures.charset())) {
                request.setCharacterEncoding(httpFeatures.charset());
                response.setCharacterEncoding(httpFeatures.charset());
                if (logger.isDebugEnabled()) {
                    logger.debug("set request/response.characterEncoding " + "by metadata:"
                            + httpFeatures.charset());
                }
            }
        }
        if (response.getContentType() == null) {
            response.setContentType("text/html;charset=UTF-8");
            if (logger.isDebugEnabled()) {
                logger.debug("set response.contentType by default:" + response.getContentType());
            }
        }
        if (request.getCharacterEncoding() == null) {
            request.setCharacterEncoding("UTF-8");
            if (logger.isDebugEnabled()) {
                logger.debug("set request.characterEncoding by default:"
                        + request.getCharacterEncoding());
            }
        }
        if (response.getCharacterEncoding() == null) {
            response.setCharacterEncoding("UTF-8");
            if (logger.isDebugEnabled()) {
                logger.debug("set response.characterEncoding by default:"
                        + response.getCharacterEncoding());
            }
        }
    }

    @Override
    public String toString() {
        if (toStringCache == null) {
            Class<?>[] parameterTypes = method.getParameterTypes();
            String appPackageName = controllerEngine.getControllerClass().getPackage().getName();
            if (appPackageName.indexOf('.') != -1) {
                appPackageName = appPackageName.substring(0, appPackageName.lastIndexOf('.'));
            }
            String methodParamNames = "";
            for (int i = 0; i < parameterTypes.length; i++) {
                if (methodParamNames.length() == 0) {
                    methodParamNames = showSimpleName(parameterTypes[i], appPackageName);
                } else {
                    methodParamNames = methodParamNames + ", "
                            + showSimpleName(parameterTypes[i], appPackageName);
                }
            }
            toStringCache = controllerEngine.getControllerClass().getName() //
                    + "." + method.getName() //
                    + "(" + methodParamNames + ")" //
                    + ":" + showSimpleName(method.getReturnType(), appPackageName) //
                    + "]";
        }
        return toStringCache;
    }

    private String showSimpleName(Class<?> parameterType, String appPackageName) {
        if (parameterType.getName().startsWith("net.paoding")
                || parameterType.getName().startsWith("java.lang")
                || parameterType.getName().startsWith("java.util")
                || parameterType.getName().startsWith(appPackageName)) {
            return parameterType.getSimpleName();
        }
        return parameterType.getName();
    }

    public void destroy() {

    }

}
